﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MvcEngine.Core.Helpers;
using System.Web.Caching;
using System.Web;
using System.Collections;

namespace MvcEngine.Core.Utils
{
    /// <summary>
    /// This class is used to manage objects cache by categories.
    /// </summary>
    public class CacheManager : ICacheManager
    {
        #region Static
        /// <summary>
        /// Gets the current cache manager. 
        /// </summary>
        /// <value>The current cache manager.</value>
        public static ICacheManager Current
        {
            get { return IoC.Resolve<ICacheManager>(); }
        }

        #endregion

        private const string KeySeparator = ":::";
        /// <summary>
        /// Default amount of minutes to store object in cache.
        /// </summary>
        public const int DefaultCacheMinutes = 60;
        /// <summary>
        /// Default cache category name.
        /// </summary>
        public const string DefaultCacheCategory = "default";

        #region Private Stuff

        private static string MakeKey(string key, string category)
        {
            return String.Concat(category, KeySeparator, key);
        }

        #endregion

        #region AddToCache

        /// <summary>
        /// Adds object to cache. Default cache category is used.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="data">Data object to store in cache.</param>
        public void AddToCache(string key, object data)
        {
            AddToCache(key, DefaultCacheCategory, data);
        }

        /// <summary>
        /// Adds object to cache. Default cache category is used.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="data">Data object to store in cache.</param>
        /// <param name="minutesToCache">Cache item timeout in minutes.</param>
        public void AddToCache(string key, object data, int minutesToCache)
        {
            AddToCache(key, DefaultCacheCategory, data, minutesToCache);
        }

        /// <summary>
        /// Adds object to cache. Default cache category is used.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="data">Data object to store in cache.</param>
        /// <param name="minutesToCache">Cache item timeout in minutes.</param>
        /// <param name="priority">Cache item priority.</param>
        public void AddToCache(string key, object data, int minutesToCache, CacheItemPriority priority)
        {
            AddToCache(key, DefaultCacheCategory, data, minutesToCache, priority);
        }

        /// <summary>
        /// Adds object to cache.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="category">Cache category.</param>
        /// <param name="data">Data object to store in cache.</param>
        public void AddToCache(string key, string category, object data)
        {
            AddToCache(key, category, data, DefaultCacheMinutes);
        }

        /// <summary>
        /// Adds object to cache.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="category">Cache category.</param>
        /// <param name="data">Data object to store in cache.</param>
        /// <param name="minutesToCache">Cache item timeout in minutes.</param>
        public void AddToCache(string key, string category, object data, int minutesToCache)
        {
            AddToCache(key, category, data, minutesToCache, CacheItemPriority.Normal);
        }

        /// <summary>
        /// Adds object to cache.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="category">Cache category.</param>
        /// <param name="data">Data object to store in cache.</param>
        /// <param name="minutesToCache">Cache item timeout in minutes.</param>
        /// <param name="priority">Cache item priority.</param>
        public void AddToCache(string key, string category, object data, int minutesToCache, CacheItemPriority priority)
        {
            Guard.ThrowIfEmptyString(key, "key", "Key cannot be empty.");
            Guard.ThrowIfEmptyString(category, "category", "Category cannot be empty.");

            lock (HttpRuntime.Cache)
            {
                string fullKey = MakeKey(key, category);
                Cache cache = HttpRuntime.Cache;
                cache.Remove(fullKey);
                cache.Add(fullKey, data, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(minutesToCache), priority, null);
            }
        }

        #endregion

        #region GetFromCache

        /// <summary>
        /// Gets an object from cache by key. Default cache category is used.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <returns></returns>
        public object GetFromCache(string key)
        {
            return GetFromCache(key, DefaultCacheCategory);
        }

        /// <summary>
        /// Gets an object from cache by key and category.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="category">Cache category.</param>
        /// <returns></returns>
        public object GetFromCache(string key, string category)
        {
            Guard.ThrowIfEmptyString(key, "key", "Cache key cannot be null or empty.");
            Guard.ThrowIfEmptyString(category, "category", "Cache category cannot be null or empty.");

            lock (HttpRuntime.Cache)
            {
                string fullKey = MakeKey(key, category);
                return HttpRuntime.Cache[fullKey];
            }
        }

        #endregion

        #region RemoveFromCache

        /// <summary>
        /// Removes object from cache. Uses default cache category to find an object.
        /// </summary>
        /// <param name="key">Cache key.</param>
        public void RemoveFromCache(string key)
        {
            RemoveFromCache(key, DefaultCacheCategory);
        }

        /// <summary>
        /// Removes object from cache.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <param name="category">Cache category.</param>
        public void RemoveFromCache(string key, string category)
        {
            Guard.ThrowIfEmptyString(key, "key", "Cache key cannot be null or empty.");
            Guard.ThrowIfEmptyString(category, "category", "Cache category cannot be null or empty.");

            lock (HttpRuntime.Cache)
            {
                string fullKey = MakeKey(key, category);
                HttpRuntime.Cache.Remove(fullKey);
            }
        }

        #endregion

        #region ClearCache

        /// <summary>
        /// Clears the cache. All items from all categories are removed.
        /// </summary>
        public void ClearCache()
        {
            RemoveCacheItems(key => true);
        }

        /// <summary>
        /// Clears the cache section related to specified category.
        /// </summary>
        /// <param name="category">Cache category.</param>
        public void ClearCache(string category)
        {
            string start = category + KeySeparator;
            RemoveCacheItems(key => key.StartsWith(start));
        }

        private static void RemoveCacheItems(Func<string, bool> keyPredicate)
        {
            lock (HttpRuntime.Cache)
            {
                Cache cache = HttpRuntime.Cache;
                var keysToRemove = from entry in cache.Cast<DictionaryEntry>()
                                   where keyPredicate(entry.Key.ToString())
                                   select entry.Key.ToString();
                foreach (var key in keysToRemove)
                {
                    cache.Remove(key);
                }
            }
        }

        #endregion

        /// <summary>
        /// This function will check the cache if the cachekey exist in the the cache. If so, it will try to match the type of the
        /// cached object with the T type. If both Match the result is returned from cache. If not the supplied actionFunction is executed
        /// and it's returned object are stored in cache linked to the cachekey. Duration the objects should be kept in the cache can be set
        /// by the minutesToCache parameter.
        /// </summary>
        /// <typeparam name="T">The type of the objects the actionFunction returns</typeparam>
        /// <param name="cachekey">Unique string used to locate and store the objects in cache.</param>
        /// <param name="minutesToCache">The number of minutes the objects are kept in the cache.</param>
        /// <param name="actionFunction">This function is called whenever the cache is empty. Results of the function will be stored in cache.</param>
        /// <returns>
        /// Object of type T either from cache or the action function.
        /// </returns>
        /// <example>
        /// This Example will check the cache for the "Customer_2" cachekey. If the cachekey is found and the return type T matched Customer the Customer
        /// object is returned. If the cachekey cannot be found in the cache the CustomerManager.GetCustomer("2") is executed and it results returned. In
        /// addition the returned results are then stored in the cache and linked to cachekey "Customer_2".
        /// <code>
        /// Customer customer = CacheManager.FromCache&lt;Customer&gt;(string.Concat("Customer_", "2"), 15, () =&gt; (Customer)CustomerManager.GetCustomer("2"));
        /// </code>
        /// </example>
        public T FromCache<T>(string cachekey, string category, int minutesToCache, Func<T> actionFunction)
        {
            Guard.ThrowIfEmptyString(cachekey, "The supplied cachekey is invalid.");
            Guard.ThrowIfLessThan(minutesToCache, 0, "The supplied minutes to cache cannot be below zero", "minutesToCache");

            //Check if result can be found in the current cache
            object result = GetFromCache(cachekey, category);
            if (result != null && typeof(T).IsAssignableFrom(result.GetType()))
                return (T)result;

            //Execute the delegate function
            T returnValue;
            returnValue = actionFunction();

            //Add the results to the cache
            if (returnValue != null)
                AddToCache(cachekey, category, returnValue, minutesToCache);

            //Return the newly fetched result
            return returnValue;
        }


        public T FromCache<T>(string cachekey, Func<T> actionFunction)
        {
            return FromCache(cachekey, DefaultCacheCategory, DefaultCacheMinutes, actionFunction);
        }

        public T FromCache<T>(string cachekey, string category, Func<T> actionFunction)
        {
            return FromCache(cachekey, category, DefaultCacheMinutes, actionFunction);
        }

        /// <summary>
        /// This function will check the cache if the cachekey exist in the the cache. If so, it will try to match the created time and type of the
        /// cached object with the T type. If both Match the result is returned from cache. If not the supplied actionFunction is executed
        /// and it's returned object are stored in cache linked to the cachekey. Duration the objects should be kept in the cache can be set
        /// by the minutesToCache parameter.
        /// </summary>
        /// <typeparam name="T">The type of the objects the actionFunction returns</typeparam>
        /// <param name="cachekey">Unique string used to locate and store the objects in cache.</param>
        /// <param name="minutesToCache">The number of minutes the objects are kept in the cache.</param>
        /// <param name="actionFunction">This function is called whenever the cache is empty. Results of the function will be stored in cache.</param>
        /// <param name="objectLastModified">If object in cache is not older, than this value, cached object will returned.
        /// If not, the supplied actionFunction is executed
        /// and it's returned object are stored in cache linked to the cachekey. If new object was added to cache, it's created time is this value.</param>
        /// <returns>
        /// Object of type T either from cache or the action function.
        /// </returns>
        public T FromCache<T>(string cachekey, int minutesToCache, Func<T> actionFunction, DateTime objectLastModified)
        {
            bool needsUpdate = true;
            string checkDateCacheKey = string.Concat(cachekey, "_chkdate");
            object result = GetFromCache(checkDateCacheKey);
            if (result != null && result.GetType() == typeof(DateTime))
            {
                DateTime storedInCacheDateTime = (DateTime)result;
                if (storedInCacheDateTime >= objectLastModified)
                {
                    //object isn't modfied yet
                    needsUpdate = false;
                }
            }

            if (needsUpdate)
            {
                T returnValue;
                returnValue = actionFunction();

                //Add the results to the cache
                AddToCache(cachekey, returnValue, minutesToCache);
                AddToCache(checkDateCacheKey, objectLastModified, minutesToCache);

                return returnValue;
            }
            else
            {
                return FromCache<T>(cachekey, DefaultCacheCategory, minutesToCache, actionFunction);
            }
        }
    }
}
